<?php
class com_common {
    /**
     * 描述 : 分页列表
     * 参数 :
     *      config : 字符串=调用共享分页, 数组=配置原始分页
     *      params : config为字符串时, 提供配置参数 {
     *          'params' : 传递给分页的参数, 默认=缺省值
     *          'config' : 替换 原始配置 非顶级 "_attr" 属性, 默认=原始配置
     *      }
     * 返回 :
     *      
     * 注明 :
     *      请求数据交互结构($_POST) : {
     *          'method' :*请求方法 类名::方法名
     *          'items'  : 总数据条目数, null=重新查询
     *          'size'   :*每页显示数
     *          'page'   :*当期页数
     *          'sort'   : 排序字段
     *          'params' : 共享参数
     *          'data'   : 响应的二维数据
     *      }
     *      静态页面元素结构     : {
     *          分页结构 : name="pagingBlock" method="请求方法" items="总条数,空为自动查询" size="每页显示数" page="当期页数" params="json共享数据" event="事件回调脚本" save="保存状态"
     *          单条数据 : name="pagingItem" 使用{`字段名`}替换数据,"`"做为转义字符
     *          空无展示 : name="pagingEmpty"
     *          请求提示 : name="pagingWait"
     *          排序按钮 : name="pagingSort" sort="以','分隔的字段名"
     *          首页按钮 : name="pagingFirst" 
     *          上一按钮 : name="pagingPrev" 
     *          下一按钮 : name="pagingNext" 
     *          尾页按钮 : name="pagingLast" 
     *          当期位置 : name="pagingPage" 须拥有innerHTML属性
     *          跳转位置 : name="pagingJump" input[type=text]
     *          数据条数 : name="pagingSize" input[type=text]
     *          额外工具 : name="pagingFbar" 默认分页使用
     *      }
     *      原始配置结构(config) : {
     *          列头    : 默认=同 "_attr.body.html" 属性 {
     *              子列头  : 同 列头 结构
     *              "_attr" : 列属性, 默认=null {
     *                  "attr" : 列头属性, 默认=""
     *                  "body" : 列体信息, 默认="", 字符串=同 "html" 属性, 对象={
     *                      "attr" : 列体属性, 默认=""
     *                      "html" : 列体内容, 默认=""
     *                  }
     *                  "html" : 列头内容, 默认="", 字符串=同 "0" 属性, 数组=[列头前内容, 列头后内容]
     *                  "sort" : 排序字段, 默认=不排序, 可以用","分割排序多个
     *                  "row"  : 纵向跨度, 默认=自动计算, table的rowspan功能
     *              }
     *          }
     *          "_atrr" : 全局属性 {
     *              "call"   : "data" 为sql语句时每页回调一次, 默认=不回调
     *              "data"   : 获取数据, 默认=[], 字符串=sql语句{`LIMIT`}可以自定义limt的位置, 数组=单页二维数据
     *              "dbKey"  : 数据库连接池, 默认=default
     *              "items"  : 数据长度, 
     *                  数字 = 总数据长度, 
     *                  sql  = 以`c`字段做为长度, 
     *                  -1   = 不计算页数, 
     *                  默认 = data是数组时为-1, sql时查询总长
     *              "params" : 终端与服务的共享数据, 默认={}
     *              "size"   : 每页数据量, 默认=_of.com.common::paging.size || 10

     *              **** 以下存在列头时生效
     *              "action" : 翻页动作html, 默认=系统内部生成
     *              "attr"   : 分页属性, 默认="", 字符串=同 "table" 属性, 数组={
     *                  "table" : 整个分页属性
     *                  "btr"   : 体 tr 属性
     *              }
     *              "empty"  : 空数据展示界面, 默认="", 字符串=同 "html" 属性, 数组={
     *                  "attr" : 界面属性, 默认=""
     *                  "html" : 界面内容, 默认=""
     *              }
     *              "event"  : 初始化和翻页前后调用的一段js代码, 默认=不调用
     *              "fbar"   : 额外的html功能条, 默认=""
     *              "method" :*通信时位置(__METHOD__), 必填
     *              "page"   : 初始页数, 默认1
     *              "save"   : 保存浏览状态, 默认=不保存, 字符串=区分不同环境的关键词(如 : 用户ID=区分不同用户)
     *              "space"  : 命名空间, 默认="", 可以实现分页嵌套分页功能
     *          }
     *      }
     *      解析过程数据(env)    : {
     *          "maxRow" : 最大深度
     *          "maxCol" : 最大宽度
     *          "parse"  : [{
     *              "name"   : 列头
     *              "parent" : 引用父节点, 顶层为null
     *              "child"  : 原始子节点, 无子为[]
     *              "attr"   : 格式化的属性 {
     *                  "attr" : 列头属性, 结构=""
     *                  "body" : 列体信息, 结构={
     *                      "attr" : ""
     *                      "html" : ""
     *                  }
     *                  "html" : 列头内容, 结构=["", ""]
     *                  "sort" : 排序字段, 结构=""
     *                  "row"  : 纵向跨度, 结构=1
     *                  "col"  :x实际宽度
     *              }
     *              "rpos"   : 计算后的纵向位置
     *          }, ...]
     *          "coor"   : 二维坐标 {
     *              纵向位置 : [对应parse值的引用, ...]
     *          }
     *          "thead"  : 生成thead标签的html
     *          "tbody"  : 生成tbody标签的html
     *          "tfoot"  : 生成tfoot标签的html
     *          "twait"  : 生成等待提示的html
     *          "empty"  : 生成无数据标签的html
     *          "file"   : 调用的文件
     *      }
     * 作者 : Edgar.lee
     */
    public static function &paging($config = null, $params = null) {
        static $global = array('attr' => null, 'conf' => null);                                                         //全局参数 {'attr' : 顶层属性, 'conf' : 替换配置}

        if( is_array($config) ) {                                                                                       //配置分页
            isset($config['_attr']) || $config = array('_attr' => $config);                                             //无列头分页

            isset($global['attr']['method']) && $config['_attr']['method'] = $global['attr']['method'];                 //修改过使用方法
            $global['attr'] = &$config['_attr'];                                                                        //顶级属性
            $attr = &$global['attr'];                                                                                   //引用全局属性
            isset($global['conf']) && $config = &$global['conf'];                                                       //替换配置是对象 && 使用替换配置
            unset($config['_attr']);

            $attr += array(                                                                                             //全局属性初始化
                'call'   => null,
                'data'   => array(),
                'dbKey'  => 'default',
                'items'  => null,
                'params' => array(),
                'size'   => 10,

                'action' => null,
                'attr'   => '',
                'empty'  => '',
                'event'  => null,
                'fbar'   => '',
                'page'   => 1,
                'save'   => null,
                'space'  => ''
            );
            $attr['space'] && $attr['space'] .= ':';                                                                    //命名空间补全
            self::slashesDeep($attr['params'], 'stripslashes');                                                           //共享参数去反斜杠

            if( !empty($config) ) {                                                                                     //存在列头, 生成分页头
                if( empty($attr['method']) ) throw new Exception("_attr.method not found");                             //method 不存在

                $env['maxRow'] = $env['maxCol'] = 0;                                                                    //最大深度和宽度
                $env['parse'] = array();                                                                                //头数据解析

                is_array($attr['attr']) || $attr['attr'] = array('table' => $attr['attr']);                             //部分全局属性初始化
                $attr['attr'] += array('table' => '', 'btr' => '');
                is_array($attr['empty']) || $attr['empty'] = array('html' => $attr['empty']);
                $attr['empty'] += array('attr' => '', 'html' => '');
                is_string($attr['action']) || $attr['action'] = '<div class="of-paging_action">' .
                    '<a name="' .$attr['space']. 'pagingFirst" class="of-paging_first" href="#">&nbsp;</a>' .
                    '<a name="' .$attr['space']. 'pagingPrev" class="of-paging_prev" href="#">&nbsp;</a>' .
                    '<a name="' .$attr['space']. 'pagingNext" class="of-paging_next" href="#">&nbsp;</a>' .
                    '<a name="' .$attr['space']. 'pagingLast" class="of-paging_last" href="#">&nbsp;</a>' .
                    '<span name="' .$attr['space']. 'pagingPage" class="of-paging_page"></span>' .
                    '<input name="' .$attr['space']. 'pagingJump" class="of-paging_jump" type="text" />' .
                    '<input name="' .$attr['space']. 'pagingSize" class="of-paging_size" type="text" />' .
                '</div>';

                foreach($config as $k => &$v) {                                                                         //数据拷贝
                    is_array($v) || $v = array('_attr' => array('body' => array('html' => $v)));
                    $env['parse'][] = array('name' => $k, 'child' => &$v, 'parent' => null, 'attr' => &$v['_attr']);
                    $v['_attr']['col'] = 0;                                                                             //实际宽度
                    unset($v['_attr']);
                }
                while( ($k = key($env['parse'])) !== null ) {                                                           //解析头关系
                    $index = &$env['parse'][$k];                                                                        //引用当前节点

                    $index['attr'] += array(                                                                            //属性初始化
                        'attr' => '',
                        'sort' => '',
                        'html' => '',
                        'row'  => 1,
                        'body' => ''
                    );
                    $index['attr']['html'] = (array)$index['attr']['html'] + array('', '');                             //列html转数组
                    is_array($index['attr']['body']) || $index['attr']['body'] = array('html' => $index['attr']['body']);
                    $index['attr']['body'] += array('attr' => '', 'html' => '');                                        //体html转数组

                    $index['rpos'] = $index['parent'] ?                                                                 //实际位置 = 有父 ? 父位置 + 父跨度 : 0
                        $index['parent']['rpos'] + $index['parent']['attr']['row'] : 0;
                    ($temp = $index['rpos'] + $index['attr']['row']) > $env['maxRow'] && $env['maxRow'] = $temp;        //更新最大深度
                    $env['coor'][$index['rpos']][] = &$index;                                                           //生成坐标

                    if( empty($index['child']) ) {                                                                      //最深节点
                        $env['tbody'][] = "<td {$index['attr']['body']['attr']}>{$index['attr']['body']['html']}</td>";
                        $env['maxCol'] += 1;                                                                            //最大宽度+1
                        do {
                            $index['attr']['col'] += 1;                                                                 //递归到父节点更新宽度
                        } while( $index = &$index['parent'] );
                    } else {                                                                                            //存在子节点
                        foreach($index['child'] as $k => &$v) {                                                         //引用子节点
                            is_array($v) || $v = array('_attr' => array('body' => array('html' => $v)));
                            $env['parse'][] = array('name' => $k, 'child' => &$v, 'parent' => &$index, 'attr' => &$v['_attr']);
                            $v['_attr']['col'] = 0;                                                                     //临时实际宽度
                            unset($v['_attr']);
                        }
                    }

                    next($env['parse']);                                                                                //指针下移
                }

                $temp = explode('::', $attr['method'], 2);                                                              //调用文件地址
                preg_match_all('@[^:_]+@', $temp[0], $temp);
                $env['flie'] = ROOT_DIR . '/' . join('/', $temp[0]) . '.php';

                foreach($env['coor'] as $kc => &$vc) {
                    $env['thead'][] = '<tr>';                                                                           //开始换行
                    foreach($vc as &$v) {
                        empty($v['child']) && $v['attr']['row'] = $env['maxRow'] - $kc;                                 //无子 && 跨度 = 最大深度 - 实际位置
                        $temp = $v['attr']['sort'] ? "name='{$attr['space']}pagingSort' sort='{$v['attr']['sort']}' class='of-paging_sort_def'" : '';
                        $env['thead'][] = "<th rowspan='{$v['attr']['row']}' colspan='{$v['attr']['col']}' {$v['attr']['attr']}>" .
                            "<font {$temp}>" .
                                $v['attr']['html'][0] .
                                $v['name'] . 
                                $v['attr']['html'][1] .
                            '</font>' .
                        "</th>";
                    }
                    $env['thead'][] = '</tr>';                                                                          //结束换行
                }

                $env['thead'] = join($env['thead']);                                                                    //完成 thead html
                $env['tbody'] = join($env['tbody']);                                                                    //完成 tbody html
                $env['tbody'] = "<tr name='{$attr['space']}pagingItem' {$attr['attr']['btr']} class='of-paging_item_odd' style='display: none;'>" . $env['tbody'] . '</tr>' .
                    "<tr name='{$attr['space']}pagingItem' {$attr['attr']['btr']} class='of-paging_item_even' style='display: none;'>" . $env['tbody'] . '</tr>';
                $env['tfoot'] = $attr['fbar'] ? "<div name='{$attr['space']}pagingFbar' class='of-paging_fbar_bro'><div class='of-paging_fbar_con'>" .
                    $attr['fbar'] . '</div><label class="of-paging_fbar_ico"></label></div>' : '';
                $env['tfoot'] = "<tr><td colspan='{$env['maxCol']}'>{$env['tfoot']}{$attr['action']}</td></tr>";        //完成 tfoot html
                $env['twait'] = "<tr><td colspan='{$env['maxCol']}'></td></tr>";                                        //完成 等待 html
                $env['empty'] = "<tr {$attr['empty']['attr']}>" .                                                       //完成 无数据 html
                    "<td colspan='{$env['maxCol']}'>{$attr['empty']['html']}</td>" .
                '</tr>';

                $temp = 'items="' .(is_numeric($attr['items']) ? $attr['items'] : ''). '"' .                            //总条数
                    ' size="' .$attr['size']. '" page="' .$attr['page']. '"' .                                          //每页显示数, 当期页数
                    ' params="' .htmlentities(json_encode($attr['params']), ENT_QUOTES, 'UTF-8'). '"' .                 //共享参数
                    ' event="' .htmlentities($attr['event'], ENT_QUOTES, 'UTF-8'). '"' .                                //回调脚本
                    ' save="' .htmlentities($attr['save'], ENT_QUOTES, 'UTF-8'). '"';                                   //保存状态
                $env['thtml'] = "<table name='{$attr['space']}pagingBlock' method='{$attr['method']}' {$temp} {$attr['attr']['table']} class='of-paging_block'>" .
                    "<thead class='of-paging_wait' name='{$attr['space']}pagingWait'>{$env['twait']}</thead>" .         //等待提示
                    "<thead class='of-paging_head'>{$env['thead']}</thead>" .                                           //分页列头
                    "<tbody class='of-paging_body'>{$env['tbody']}</tbody>" .                                           //分页数据
                    "<tbody class='of-paging_empty' name='{$attr['space']}pagingEmpty'>{$env['empty']}</tbody>" .       //空数据提示
                    "<tfoot class='of-paging_foot'>{$env['tfoot']}</tfoot>" .                                           //分页尾数据
                '</table>';
            }

            return $env['thtml'];
        } else {                                                                                                        //调用分页, 字符串=直调, null=POST
            if( $config === null ) {                                                                                    //POST 请求
                $post = $_POST;                                                                                         //复制post数据
                $config = $post['method'];
                $params['config'] = array();                                                                            //不生成节点
                if( isset($post['params'][0]) ) {
                    $params['params'] = json_decode(stripslashes($post['params']), true);                               //共享参数转成json
                    self::slashesDeep($params['params']);                                                                 //防注入
                }
            }

            if( true ) {
                isset($params['config']) && $global['conf'] = &$params['config'];                                       //重写配置
                $global['attr']['method'] = $config;                                                                    //使用方法

                $temp = explode('::', $config);
                $result = call_user_func_array(
                    array(new $temp[0], $temp[1]), 
                    isset($params['params']) ? array(&$params['params']) : array()
                );

                if( isset($post) ) {                                                                                    //开始查询数据
                    $attr = &$global['attr'];                                                                           //引用属性

                    $post['params'] = $attr['params'];                                                                  //更新共享参数
                    $post['page'] < 1 && $post['page'] = 1;                                                             //当期页初始化
                    $post['size'] < 1 && $post['size'] = $attr['size'];                                                 //单页数初始化
                    if( empty($post['items']) ) {                                                                       //重新计算分页
                        if( is_numeric($attr['items']) ) {                                                              //数字格式
                            $post['items'] = $attr['items'] . '';
                        } else if( is_string($attr['items']) ) {                                                        //sql语句
                            $post['items'] = of_db::sql($attr['items'], $attr['dbKey']);
                            $post['items'] = $post['items'][0]['c'];                                                    //获取总数量
                        } else if( is_array($attr['data']) ) {                                                          //数组数据
                            $post['items'] = '-1';
                        }
                    }

                    if( is_string($attr['data']) ) {                                                                    //执行sql读取数据
                        $temp = array(
                            'pos'    => false,                                                                          //关键词位置
                            'offset' => 0,                                                                              //偏移位置
                            'keys'   => array('"' => true, '\'' => true),                                               //定位关键词
                            'match'  => null                                                                            //匹配信息
                        );
                        while(true) {
                            if( $temp['match'] = of_base_com_str::strArrPos($attr['data'], $temp['keys'], $temp['offset']) ) {
                                if( ($temp['pos'] = strpos(
                                        substr($attr['data'], $temp['offset'], $temp['match']['position'] - $temp['offset']),
                                        '{`LIMIT`}'
                                    )) !== false
                                ) {                                                                                     //找到LIMIT
                                    $temp['pos'] += $temp['offset'];
                                    break;
                                }

                                $temp['match'] = of_base_com_str::strArrPos($attr['data'], array($temp['match']['match'] => true), $temp['match']['position'] + 1);
                                if( $temp['match'] ) {
                                    $temp['offset'] = $temp['match']['position'] + 1;
                                } else {                                                                                //语法错误
                                    break;
                                }
                            } else {                                                                                    //没找到关键词
                                $temp['pos'] = strpos(substr($attr['data'], $temp['offset']), '{`LIMIT`}');
                                $temp['pos'] === false || $temp['pos'] += $temp['offset'];                              //没找到LIMIT || 真实位置
                                break;
                            }
                        }

                        $temp['pos'] || $temp['pos'] = strlen($attr['data']);
                        preg_match("@^(.{{$temp['pos']}})(?:|.{9}(.*))()$@s", $attr['data'], $temp);                    //$temp=LIMIT左[1]右[2]分割
                        if( preg_match('@ORDER\s+BY(\s+[^()]+)$@i', $temp[1], $temp[3], PREG_OFFSET_CAPTURE) ) {        //提取内置排序
                            $temp[1] = substr($temp[1], 0, $temp[3][0][1]);
                            $post['sort'] = empty($post['sort']) ? $temp[3][1][0] : "{$post['sort']}, {$temp[3][1][0]}";
                        }

                        if( empty($post['items']) ) {
                            $temp['uStr'] = strtoupper($temp[1] . $temp[2]);                                            //去掉 SQL LIMIT 并转换成大写
                            $temp['keys'] = array(array('"' => true, '\'' => true, '(' => false, 'FROM' => false));     //定位 FROM 关键栈
                            $temp['sLen'] = $temp['offset'] = strpos($temp['uStr'], 'SELECT') + 6;                      //SELECT后的位置 = 起始偏移量

                            while( $temp['match'] = of_base_com_str::strArrPos($temp['uStr'], end($temp['keys']), $temp['offset']) ) {
                                switch( $temp['match']['match'] ) {
                                    case 'FROM':                                                                        //定位成功
                                        $post['items'] = substr($temp[1], 0, $temp['sLen']) .
                                            ' SQL_CALC_FOUND_ROWS 1 c ' .
                                            substr($temp[1]. $temp[2], $temp['match']['position']) .
                                            ' LIMIT 0 UNION ALL SELECT FOUND_ROWS()';
                                        break 2;
                                    case '('   :                                                                        //寻找 ")"
                                        $temp['keys'][] = array('"' => true, '\'' => true, ')' => false);
                                        break;
                                    case ')'   :                                                                        //结束 ")"
                                        array_pop($temp['keys']);                                                       //从关键栈中删除括号查询
                                        break;
                                    default    :                                                                        //结束 " 与 '
                                        if( isset($temp['quote']) ) {                                                   //已开启引号
                                            array_pop($temp['keys']);                                                   //从关键栈中删除引号查询
                                            unset($temp['quote']);
                                        } else {                                                                        //需要开启引号
                                            $temp['keys'][] = array($temp['match']['match'] => true);
                                            $temp['quote'] = true;                                                      //标记引号开启
                                        }
                                }
                                $temp['offset'] = $temp['match']['position'] + 1;                                       //更新偏移位置
                            }

                            $post['items'] = of_db::sql($post['items'], $attr['dbKey']);                                //提取SQL长度
                            $post['items'] = $post['items'][0]['c'];                                                    //获取总数量
                        }

                        if( $post['items'] > -1 ) {                                                                     //校正 $post['page']
                            ($temp[0] = ceil($post['items'] / $post['size'])) < $post['page'] &&                        //总页数 < 当期页数 && 当前页数 = 总页数
                                $post['page'] = $temp[0] ? $temp[0] : 1;                                                //纠正page < 1, ceil 返回 float
                        }

                        empty($post['sort']) || $temp[$temp[2] ? 2 : 1] .= ' ORDER BY ' . $post['sort'];                //存在排序
                        $temp = "{$temp[1]} LIMIT " . ($post['page'] - 1) * $post['size'] . ", {$post['size']}{$temp[2]}";
                        $attr['data'] = of_db::sql($temp, $attr['dbKey']);                                              //查询data数据

                        if( $attr['call'] ) {                                                                           //数据回调
                            $attr['call'] = array('callback' => $attr['call']);
                            self::callFunc($attr['call'], array('data' => &$attr['data']));
                        }
                    } else if( $post['items'] > -1 ) {                                                                  //校正 $post['page']
                        ($temp = ceil($post['items'] / $post['size'])) < $post['page'] && $post['page'] = $temp;        //总页数 < 当期页数 && 当前页数 = 总页数
                    }

                    $post['data'] = &$attr['data'];                                                                     //响应数据
                    echo json_encode($post);
                }

                unset($global['attr'], $global['conf']);                                                                //重置全局配置
            } else {                                                                                                    //无权限调用
                throw new Exception('"_of.com.common::paging.check" Permission denied');
            }

            return $result;
        }
    }

    /********************************************************************工具类*******************************************
     * 描述 : 深度加/删反斜杠
     * 参数 :
     *     &data : 指定替换的数组
     *      func : addslashes(默认)=添加反斜杠, stripslashes=删除反斜杠
     * 作者 : Edgar.lee
     */
    public static function &slashesDeep(&$data, $func = 'addslashes') {
        $waitList = array(&$data);                                                                                      //待处理列表

        do {
            $wk = key($waitList);
            $wv = &$waitList[$wk];
            unset($waitList[$wk]);

            if( is_array($wv) ) {
                $result = array();                                                                                      //结果列表
                foreach($wv as $k => &$v) {
                    $result[$func($k)] = &$v;
                    $waitList[] = &$v;
                }
                $wv = $result;
            } else if( is_string($wv) ) {
                $wv = $func($wv);
            }
        } while( !empty($waitList) );

        return $data;
    }

    /**
     * 描述 : 回调函数
     * 参数 :
     *     &call   : 调用函数的信息 {
     *          "callback" : 符合 is_callable 调用规范
     *          "params"   : call_user_func_array 格式的参数,用[_]键指定类名位置
     *      }
     *      params : 传入到 [_] 位置的参数
     * 返回 :
     *      返回 调用函数 返回的数据
     * 作者 : Edgar.lee
     */
    public static function callFunc(&$call, $params = null) {
        if( isset($call['callback']) ) {
            isset($call['params']) && $temp = $call['params'];
            $temp['_'] = &$params;

            return call_user_func_array($call['callback'], $temp);
        }
    }
}

if( isset($_GET['c']) && $_GET['c'] === 'com_common' ) {
    include 'tool.php';
    $obj = new com_common;
    $obj->paging();
}